﻿using System;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Web.Mvc;
using System.Web.Routing;
using Microsoft.Web.Mvc;

namespace Entropy.Ext
{
	public static class ActionLinkExtension
	{

		// This method contains some heuristics that will help determine the correct action name from a given MethodInfo
		// assuming the default sync / async invokers are in use. The logic's not foolproof, but it should be good enough
		// for most uses.
		private static string GetTargetActionName(MethodInfo methodInfo)
		{
			string methodName = methodInfo.Name;

			// do we know this not to be an action?
			if (methodInfo.IsDefined(typeof(NonActionAttribute), true /* inherit */))
			{
				throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture,
					"The method '{0}' is marked [NonAction] and cannot be called directly.", methodName));
			}

			// has this been renamed?
			ActionNameAttribute nameAttr = methodInfo.GetCustomAttributes(typeof(ActionNameAttribute), true /* inherit */).OfType<ActionNameAttribute>().FirstOrDefault();
			if (nameAttr != null)
			{
				return nameAttr.Name;
			}

			// targeting an async action?
			Debug.Assert(methodInfo.DeclaringType != null);
			if (methodInfo.DeclaringType.IsSubclassOf(typeof(AsyncController)))
			{
				if (methodName.EndsWith("Async", StringComparison.OrdinalIgnoreCase))
				{
					return methodName.Substring(0, methodName.Length - "Async".Length);
				}
				if (methodName.EndsWith("Completed", StringComparison.OrdinalIgnoreCase))
				{
					throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture,
						"The method '{0}' is an asynchronous completion method and cannot be called directly.", methodName));
				}
			}

			// fallback
			return methodName;
		}

		internal static RouteValueDictionary GetRouteValuesFromExpression<TController>(Expression<Action<TController>> action) where TController : Controller
		{
			if (action == null)
				throw new ArgumentNullException("action");

			return RouteValuesFromMethodCall<TController>(action.Body as MethodCallExpression);
		}

		internal static RouteValueDictionary GetRouteValuesFromExpression<TController, TModel>(Expression<Func<TController, StoreResult<TModel>>> action) where TController : Controller
		{
			if (action == null)
				throw new ArgumentNullException("action");

			return RouteValuesFromMethodCall<TController>(action.Body as MethodCallExpression);
		}

		private static RouteValueDictionary RouteValuesFromMethodCall<TController>(MethodCallExpression call) where TController : Controller
		{
			if (call == null)
			{
				throw new ArgumentException("Expression must be a method call.", "action");
			}

			string controllerName = typeof (TController).Name;
			if (!controllerName.EndsWith("Controller", StringComparison.OrdinalIgnoreCase))
			{
				throw new ArgumentException("Controller name must end in 'Controller'.", "action");
			}
			controllerName = controllerName.Substring(0, controllerName.Length - "Controller".Length);
			if (controllerName.Length == 0)
			{
				throw new ArgumentException("Cannot route to class named 'Controller'.", "action");
			}

			// TODO: How do we know that this method is even web callable?
			//      For now, we just let the call itself throw an exception.

			string actionName = GetTargetActionName(call.Method);

			var rvd = new RouteValueDictionary {{"Controller", controllerName}, {"Action", actionName}};

			var areaAttr =
				typeof (TController).GetCustomAttributes(typeof (ActionLinkAreaAttribute), true /* inherit */).FirstOrDefault() as
				ActionLinkAreaAttribute;
			if (areaAttr != null)
			{
				string areaName = areaAttr.Area;
				rvd.Add("Area", areaName);
			}

			AddParameterValuesFromExpressionToDictionary(rvd, call);
			return rvd;
		}

		static void AddParameterValuesFromExpressionToDictionary(RouteValueDictionary rvd, MethodCallExpression call)
		{
			ParameterInfo[] parameters = call.Method.GetParameters();

			if (parameters.Length > 0)
			{
				for (int i = 0; i < parameters.Length; i++)
				{
					Expression arg = call.Arguments[i];
					var ce = arg as ConstantExpression;
					// If argument is a constant expression, just get the value
					object value = ce != null ? ce.Value : CachedExpressionCompiler.Evaluate(arg);
					rvd.Add(parameters[i].Name, value);
				}
			}
		}

		internal static string StringifyActionUrl(HtmlHelper helper, RouteValueDictionary routeValues, bool includeParameters)
		{
			VirtualPathData vpd = helper.RouteCollection.GetVirtualPathForArea(helper.ViewContext.RequestContext, routeValues);
			var url = (vpd == null) ? null : vpd.VirtualPath;
			if (url != null && !includeParameters)
				// truncate parameters
				url = url.Split(new[] {'?'})[0];
			return url;
		}

		public static string ActionUrl<TController>(this HtmlHelper helper, Expression<Action<TController>> action, bool includeParameters = false) where TController : Controller
		{
			return StringifyActionUrl(helper, GetRouteValuesFromExpression(action), includeParameters);
		}

	}
}